﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.IO.IsolatedStorage;
using System.IO;
using System.Runtime.Serialization;
using Passwords.Exceptions;
using System.Diagnostics;
using Passwords.Models;
using System.Security.Cryptography;

namespace Passwords.DAL
{
    /// <summary>
    /// 
    /// </summary>
    /// <remarks>
    /// The isolated storage is structured as followed:
    /// 
    /// /Current/                                                => main folder with actual passwords.
    /// /Current/PasswordHash.bin                     => hash key of the password begin used.
    /// /Current/Categories/                               => folder containing al categories
    ///                {id1}.bin
    ///                {id2}.bin
    ///                {idn}.bin
    /// /Current/Passwords/                              => folder containing all password bodies (all password data except fields).
    ///               {id1}.bin
    ///                {id2}.bin
    ///                {idn}.bin
    /// /Current/Fields/                                     => folder containing all passwords fields.
    ///               {id1}.bin
    ///               {id2}.bin
    ///               {idn}.bin
    /// /Current/Sync/                                   => folder with sync table for each synced device (phone).
    /// /Current/Sync({guid}.sync                    => Sync table for a device identified as {guid}
    ///               
    /// /Backup1/...                                         => like /Current/ but a backup of data.
    /// /Backup2/...
    /// 
    ///               
    /// where id is either the id of the password or category, and it's usually (not always!) a guid.
    /// </remarks>
    public class DataContext : IDisposable
    {
        #region const

        const string SyncFolder = "Sync";
        const string TempFolder = "Temp";
        const string PasswordsFolder = "Passwords";
        const string CategoriesFolder = "Categories";
        const string FieldsFolder = "Fields";
        const string PasswordHashFile = "PasswordHash.bin";
        const string PasswordExt = ".bin";
        const string CategoryExt = ".bin";
        const string FieldExt = ".bin";
        const string SyncExt = ".sync";


        #endregion
        #region fields

        private string Root = "Current";
        private byte[] passwordHash;
        private IsolatedStorageFile storage;
        private DataContractSerializer passwordSerializer;
        private DataContractSerializer categorySerializer;
        private DataContractSerializer fieldsSerializer;
        private DataContractSerializer syncSerializer;

        #endregion
        #region ctor/dtor

        public DataContext()
            : this("Current")
        {
        }

        public DataContext(string baseFolderName)
            : base()
        {

            this.Root = baseFolderName;
            storage = DALUtil.GetStorage();

            passwordSerializer = new DataContractSerializer(typeof(PasswordContract));
            categorySerializer = new DataContractSerializer(typeof(CategoryContract));
            fieldsSerializer = new DataContractSerializer(typeof(FieldCollection));

            CurrentPasswordHashFile = Path.Combine(Root, PasswordHashFile);
            CurrentCategoriesFolder = Path.Combine(Root, CategoriesFolder);
            CurrentSyncFolder = Path.Combine(Root, SyncFolder);
            CurrentPasswordsFolder = Path.Combine(Root, PasswordsFolder);
            CurrentFieldsFolder = Path.Combine(Root, FieldsFolder);
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            if (Password != null)
            {
                Password.Dispose();
                Password = null;
            }
            if (Aes != null) Aes.Dispose();
            Aes = null;
            if (storage != null)
            {
                storage.Dispose();
                storage = null;
            }
        }

        ~DataContext()
        {
            Dispose();
        }

        #endregion
        #region props

        protected DataContractSerializer SyncSerializer
        {
            get { return syncSerializer ?? (syncSerializer = new DataContractSerializer(typeof(SyncTable))); }
        }

        public SecureString Password { get; private set; }

        public AesManaged Aes = new AesManaged();

        public byte[] PasswordHash { get { return passwordHash; } }

        protected string CurrentCategoriesFolder { get; private set; }
        protected string CurrentPasswordsFolder { get; private set; }
        protected string CurrentFieldsFolder { get; private set; }
        protected string CurrentPasswordHashFile { get; private set; }
        protected string CurrentSyncFolder { get; private set; }

        public bool IsLogged { get; private set; }

        #endregion
        #region methods

        public IEnumerable<string> ReadCategoryIds()
        {
            if (!IsLogged) return null;
            string pattern = Path.Combine(CurrentCategoriesFolder, "*" + CategoryExt);
            string[] fields = storage.GetFileNames(pattern);
            return fields.Select(s => Path.GetFileNameWithoutExtension(s));
        }

        public IEnumerable<string> ReadPasswordIds()
        {
            if (!IsLogged) return null;
            string pattern = Path.Combine(CurrentPasswordsFolder, "*" + PasswordExt);
            string[] fields = storage.GetFileNames(pattern);
            return fields.Select(s => Path.GetFileNameWithoutExtension(s));
        }

        public PasswordContract ReadPassword(Guid guid)
        {
            return ReadPassword(guid.ToString());
        }

        public PasswordContract ReadPassword(string passwordId)
        {
            if (!IsLogged) throw new NotLoggedException();
            if (string.IsNullOrEmpty(passwordId)) throw new ArgumentNullException("passwordId");

            string path = Path.Combine(CurrentPasswordsFolder, passwordId + PasswordExt);
            if (!storage.FileExists(path)) return null;

            try
            {
                using (Stream stream = storage.OpenFile(path, FileMode.Open, FileAccess.Read))
                {
                    using (ICryptoTransform transform = Aes.CreateDecryptor())
                    {
                        using (CryptoStream cStream = new CryptoStream(stream, transform, CryptoStreamMode.Read))
                        {
                            PasswordContract password = passwordSerializer.ReadObject(cStream) as PasswordContract;
                            return password;
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Trace.WriteLine(e.Message);
                return null;
            }
        }

        public CategoryContract ReadCategory(string categoryId)
        {
            if (!IsLogged) throw new NotLoggedException();
            if (string.IsNullOrEmpty(categoryId)) throw new ArgumentNullException("categoryId");

            string path = Path.Combine(CurrentCategoriesFolder, categoryId + CategoryExt);
            if (!storage.FileExists(path)) return null;

            try
            {
                using (Stream stream = storage.OpenFile(path, FileMode.Open, FileAccess.Read))
                {
                    CategoryContract category = categorySerializer.ReadObject(stream) as CategoryContract;
                    //                category.Context = this;
                    return category;
                }
            }
            catch (Exception ex)
            {
                Trace.Write(ex.Message);
                return null;
            }
        }

        public IEnumerable<FieldContract> ReadPasswordFields(string passwordId)
        {
            if (!IsLogged) throw new NotLoggedException();
            if (string.IsNullOrEmpty(passwordId)) throw new ArgumentNullException("passwordId");

            string path = Path.Combine(CurrentFieldsFolder, passwordId + FieldExt);
            if (!storage.FileExists(path)) return null;

            try
            {
                using (Stream stream = storage.OpenFile(path, FileMode.Open, FileAccess.Read))
                {
                    using (ICryptoTransform transform = Aes.CreateDecryptor())
                    {
                        using (CryptoStream cStream = new CryptoStream(stream, transform, CryptoStreamMode.Read))
                        {
                            FieldCollection fields = fieldsSerializer.ReadObject(cStream) as FieldCollection;
                            return fields.Fields;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.Write(ex.Message);
#if DEBUG
                using (Stream stream = storage.OpenFile(path, FileMode.Open, FileAccess.Read))
                {
                    var tr = new StreamReader(stream);
                    string s = tr.ReadToEnd();
                    Trace.WriteLine(s);
                }
#endif
                throw;

            }
        }

        public bool SavePassword(PasswordContract password, bool bodyOnly = false, bool createCategoryIds = true)
        {
            if (!IsLogged) throw new NotLoggedException();
            if (password == null) throw new ArgumentNullException("password");

            if (password.Guid == Guid.Empty)
            {
                password.Guid = Guid.NewGuid();
            }
            password.Modified = DateTime.Now;

            string passwordId = password.Guid.ToString();
            string passwordPath = Path.Combine(CurrentPasswordsFolder, passwordId + PasswordExt);

            if (createCategoryIds) password.CategoryIds = password.Categories.Select(c => c.Id).ToArray();

            using (Stream stream = storage.OpenFile(passwordPath, FileMode.Create, FileAccess.Write))
            {
                using (ICryptoTransform transform = Aes.CreateEncryptor())
                {
                    using (CryptoStream cStream = new CryptoStream(stream, transform, CryptoStreamMode.Write))
                    {
                        passwordSerializer.WriteObject(cStream, password);
                    }
                }
            }

            if (!bodyOnly)
            {
                FieldCollection fields = new FieldCollection { Fields = password.Fields.ToArray() };
                SavePasswordFields(passwordId, fields);
            }
            else password.IsModified = false;

            return true;
        }

        private void SavePasswordFields(string passwordId, FieldCollection fields)
        {
            string fieldsPath = Path.Combine(CurrentFieldsFolder, passwordId + FieldExt);
            using (Stream stream = storage.OpenFile(fieldsPath, FileMode.Create, FileAccess.Write))
            {
                using (ICryptoTransform transform = Aes.CreateEncryptor())
                {
                    using (CryptoStream cStream = new CryptoStream(stream, transform, CryptoStreamMode.Write))
                    {
                        fieldsSerializer.WriteObject(cStream, fields);
                    }
                }
            }
        }

        public bool SaveCategory(CategoryContract category)
        {
            if (!IsLogged) throw new NotLoggedException();
            if (null == category) throw new ArgumentNullException("category");

            if (string.IsNullOrEmpty(category.Id))
            {
                category.Id = Guid.NewGuid().ToString();
                //                category.Context = this;
            }
            string categoryId = category.Id;

            string path = Path.Combine(CurrentCategoriesFolder, categoryId + CategoryExt);

            using (Stream stream = storage.OpenFile(path, FileMode.Create, FileAccess.Write))
            {
                categorySerializer.WriteObject(stream, category);
            }
            category.IsModified = false;

            return true;
        }

        public bool DeletePassword(Guid guid)
        {
            return DeletePassword(guid.ToString());
        }

        public bool DeletePassword(string passwordId)
        {
            if (!IsLogged) throw new NotLoggedException();
            if (string.IsNullOrEmpty(passwordId)) throw new ArgumentNullException("passwordId");

            string bodyfileName = Path.Combine(CurrentPasswordsFolder, passwordId + PasswordExt);
            string fieldsFieldName = Path.Combine(CurrentFieldsFolder, passwordId + FieldExt);
            storage.DeleteFile(fieldsFieldName);
            storage.DeleteFile(bodyfileName);
            return true;
        }

        public bool DeleteCategory(string categoryId)
        {
            if (!IsLogged) throw new NotLoggedException();
            if (string.IsNullOrEmpty(categoryId)) throw new ArgumentNullException("categoryId");

            string categoryFileName = Path.Combine(CurrentCategoriesFolder, categoryId + PasswordExt);
            if (storage.FileExists(categoryFileName))
            {
                storage.DeleteFile(categoryFileName);
            }
            return true;
        }

        public bool Login(SecureString password)
        {
            if (Password != null) Password.Dispose();
            this.Password = password.Copy();
            this.Password.MakeReadOnly();
            this.passwordHash = password.ComputeHash();

            byte[] savedHash = ReadPasswordHash();
            IsLogged = DALUtil.IsEqual(savedHash, passwordHash);

            if (IsLogged)
            {
                byte[] cryptoKey, cryptoIV;
                DALUtil.CreateCryptoKey(password, out cryptoKey, out cryptoIV);
                Aes.Key = cryptoKey;
                Aes.IV = cryptoIV;


                CreateFolders();
            }
            return IsLogged;
        }

        public bool Login(string password)
        {
            this.passwordHash = password.ComputeHash();

            byte[] savedHash = ReadPasswordHash();
            IsLogged = DALUtil.IsEqual(savedHash, passwordHash);

            if (IsLogged)
            {
                byte[] cryptoKey, cryptoIV;
                DALUtil.CreateCryptoKey(password, out cryptoKey, out cryptoIV);
                Aes.Key = cryptoKey;
                Aes.IV = cryptoIV;


                CreateFolders();
            }
            return IsLogged;
        }

        private void CreateFolders()
        {
            if (!storage.DirectoryExists(Root)) storage.CreateDirectory(Root);
            if (!storage.DirectoryExists(CurrentCategoriesFolder)) storage.CreateDirectory(CurrentCategoriesFolder);
            if (!storage.DirectoryExists(CurrentPasswordsFolder)) storage.CreateDirectory(CurrentPasswordsFolder);
            if (!storage.DirectoryExists(CurrentFieldsFolder)) storage.CreateDirectory(CurrentFieldsFolder);
        }

        private byte[] ReadPasswordHash()
        {
            if (!storage.FileExists(CurrentPasswordHashFile)) return string.Empty.ComputeHash();
            using (Stream stream = storage.OpenFile(CurrentPasswordHashFile, FileMode.Open))
            {
                int len = (int)stream.Length;
                using (BinaryReader reader = new BinaryReader(stream))
                {
                    return reader.ReadBytes(len);
                }
            }
        }

        private void WritePasswordHash(string password)
        {
            byte[] hash = password.ComputeHash();
            using (Stream stream = storage.OpenFile(CurrentPasswordHashFile, FileMode.Create))
            {
                stream.Write(hash, 0, hash.Length);
            }
        }

        private void WritePasswordHash(SecureString password)
        {
            byte[] hash = password.ComputeHash();
            WritePasswordHash(hash);
        }

        public void WritePasswordHash(byte[] hash)
        {
            CreateFolders();
            using (Stream stream = storage.OpenFile(CurrentPasswordHashFile, FileMode.Create))
            {
                stream.Write(hash, 0, hash.Length);
            }
        }

        public bool Logout()
        {
            Aes.Key.Zero();
            Aes.IV.Zero();
            IsLogged = false;
            return true;
        }

        public PasswordError ChangePassword(SecureString oldPassword, SecureString newPassword)
        {
            if (newPassword.Length < 4) return PasswordError.TooShort;
            if (!DALUtil.IsEqual(oldPassword, newPassword))
            {
                if (!(DALUtil.IsEqual(oldPassword.ComputeHash(), ReadPasswordHash()))) return PasswordError.WrongPassword;
                ClearTempFolder();
                if (storage.DirectoryExists(Root))
                {
                    storage.MoveDirectory(Root, TempFolder);
                }
                try
                {
                    using (DataContext temp = new DataContext(TempFolder))
                    {
                        if (temp.Login(oldPassword))
                        {
                            CreateFolders();
                            WritePasswordHash(newPassword);
                            if (Login(newPassword))
                            {
                                foreach (string id in temp.ReadCategoryIds())
                                {
                                    CategoryContract c = temp.ReadCategory(id);
                                    if (c != null) SaveCategory(c);
                                }
                                FieldCollection fc = new FieldCollection();
                                foreach (string id in temp.ReadPasswordIds())
                                {
                                    PasswordContract p = temp.ReadPassword(id);
                                    SavePassword(p, bodyOnly: true, createCategoryIds: false);

                                    fc.Fields = temp.ReadPasswordFields(id).ToArray();
                                    SavePasswordFields(id, fc);
                                }
                                ClearTempFolder();
                                return PasswordError.Success;
                            }
                        }
                        else return PasswordError.WrongPassword;
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                    DeleteDatabase();
                    storage.MoveDirectory(TempFolder, Root);
                    throw;
                }

            }
            return PasswordError.NoMatch;
        }

        public bool ChangePassword(string oldPassword, string newPassword)
        {
            if (oldPassword != newPassword)
            {
                if (!(DALUtil.IsEqual(oldPassword.ComputeHash(), ReadPasswordHash()))) return false;
                ClearTempFolder();
                if (storage.DirectoryExists(Root))
                {
                    storage.MoveDirectory(Root, TempFolder);
                }
                try
                {
                    using (DataContext temp = new DataContext(TempFolder))
                    {
                        if (temp.Login(oldPassword))
                        {
                            CreateFolders();
                            WritePasswordHash(newPassword);
                            if (Login(newPassword))
                            {
                                foreach (string id in temp.ReadCategoryIds())
                                {
                                    CategoryContract c = temp.ReadCategory(id);
                                    SaveCategory(c);
                                }
                                FieldCollection fc = new FieldCollection();
                                foreach (string id in temp.ReadPasswordIds())
                                {
                                    PasswordContract p = temp.ReadPassword(id);
                                    SavePassword(p, bodyOnly: true);

                                    fc.Fields = temp.ReadPasswordFields(id).ToArray();
                                    SavePasswordFields(id, fc);
                                }
                                ClearTempFolder();
                                return true;
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                    DeleteDatabase();
                    storage.MoveDirectory(TempFolder, Root);
                    throw;
                }

            }
            return false;
        }

        private void ClearTempFolder()
        {
            if (storage.DirectoryExists(TempFolder))
            {
                DeleteFolders(TempFolder);
                storage.DeleteDirectory(TempFolder);
            }
        }

        public void DeleteDatabase()
        {
            if (storage.DirectoryExists(Root))
            {
                DeleteFolders(Root);
                storage.DeleteDirectory(Root);
            }
        }

        private void DeleteFolders(string path)
        {
            string pattern = Path.Combine(path, "*");
            if (storage.DirectoryExists(path))
            {
                var folders = storage.GetDirectoryNames(pattern);
                foreach (var folder in folders)
                {
                    string subPath = Path.Combine(path, folder);
                    DeleteFolders(subPath);
                    storage.DeleteDirectory(subPath);
                }
                var files = storage.GetFileNames(pattern);
                foreach (var file in files)
                {
                    string subPath = Path.Combine(path, file);
                    storage.DeleteFile(subPath);
                }
            }
        }

        public void RestoreBackup(Services.Backup backup)
        {
            if (backup == null) throw new ArgumentNullException("backup");
            DeleteDatabase();
            CreateFolders();
            WritePasswordHash(backup.PasswordHash);
            foreach (CategoryContract c in backup.Categories)
            {
                SaveCategory(c);
            }
            foreach (PasswordContract p in backup.Passwords)
            {
                p.Fields = new ObservableChildCollection<FieldContract>(p, DecryptFields(p));
                SavePassword(p, bodyOnly: false, createCategoryIds: false);
            }
        }


        private IEnumerable<FieldContract> DecryptFields(PasswordContract password)
        {
            if (password == null) throw new ArgumentNullException("password");
            if (password.EncryptedDetails == null) return null;
            DataContractSerializer serializer = new DataContractSerializer(typeof(FieldContract[]));
            try
            {
                using (MemoryStream ms = new MemoryStream(password.EncryptedDetails))
                {
                    ms.Position = 0;
                    using (ICryptoTransform transform = Aes.CreateDecryptor())
                    {
                        using (Stream stream = new CryptoStream(ms, transform, CryptoStreamMode.Read))
                        {
                            FieldContract[] fc = serializer.ReadObject(stream) as FieldContract[];
                            return fc;
                        }
                    }
                }
            }
            catch
            {
                Debugger.Break();
                return null;
            }
        }

        public SyncTable GetSyncTable(Guid deviceId)
        {
            if (!storage.DirectoryExists(CurrentSyncFolder)) return null;

            string fileName = Path.Combine(CurrentSyncFolder, deviceId.ToString() + SyncExt);

            if (!storage.FileExists(fileName)) return null;

            using (Stream stream = storage.OpenFile(fileName, FileMode.Open))
            {
                SyncTable table = SyncSerializer.ReadObject(stream) as SyncTable;
                return table;
            }
        }

        public void SaveSyncTable(SyncTable table)
        {
            if (!storage.DirectoryExists(CurrentSyncFolder)) storage.CreateDirectory(CurrentSyncFolder);
            string fileName = Path.Combine(CurrentSyncFolder, table.DeviceId.ToString() + SyncExt);

            using (Stream stream = storage.OpenFile(fileName, FileMode.Create, FileAccess.Write))
            {
                SyncSerializer.WriteObject(stream, table);
            }

        }

        #endregion


    }
}
